package org.iabako.server.dao;

import org.apache.log4j.Logger;
import org.iabako.server.serviceimpl.security.SessionUser;
import org.iabako.shared.entity.business.*;
import org.iabako.shared.entity.user.Enterprise;
import org.iabako.shared.entity.user.User;
import org.iabako.shared.tools.GenericTools;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.lang.reflect.ParameterizedType;
import java.util.List;

public abstract class JpaDAO<K, E> {

    static Logger log = Logger.getLogger(JpaDAO.class.getName());

    protected Class<E> entityClass;

    @PersistenceContext(unitName = "MyPersistenceUnit")
    protected EntityManager entityManager;

    @Autowired
    SessionUser sessionUser;

    @SuppressWarnings("unchecked")
    public JpaDAO() {
        ParameterizedType genericSuperclass = (ParameterizedType) getClass()
                .getGenericSuperclass();
        this.entityClass = (Class<E>) genericSuperclass
                .getActualTypeArguments()[1];
    }

    protected User getSessionUser() {
        try {
            return sessionUser != null ? sessionUser.getUser() : null;
        } catch (BeanCreationException exception) {
            return null;
        }
    }

    public boolean isUniqueNumber(BusinessEntity entity) {
        log.info("Unique Id verification for " + this.entityClass.getSimpleName());

        Enterprise enterprise = getSessionUser() != null ?
                getSessionUser().getEnterprise() : entity.getEnterprise();

        List<Long> count;
        if (entity.getNumber() == null) {
            entity.setNumber(getNextAvailableId());
            return true;
        } else {
            count = getEntityIdsByNumber(enterprise, entity.getNumber());
        }

        if (count == null || count.size() > 1) {
            //how could this happen ?
            return false;
        }

        //Unique if none with same number or the only one detected is itself
        return count.size() < 1 || count.get(0).equals(entity.getId());
    }

    public String getNextAvailableId() {
        log.info("GET next available id for " + this.entityClass.getSimpleName());

        User user = getSessionUser();

        String query = getLastNumberQuery();
        String lastId = (String) entityManager.createQuery(query).
                setParameter("enterprises", user.getEnterprise().getAllRelatedEnterprises()).
                getSingleResult();

        return getNextCheckedNumber(lastId);
    }

    protected String getNextCheckedNumber(String lastId) {
        String nextNumber = getNextNumber(lastId);
        List<Long> numberUsedList = getEntityIdsByNumber(getSessionUser().getEnterprise(), nextNumber);
        boolean numberNotUsed = numberUsedList == null || numberUsedList.isEmpty();

        return numberNotUsed ? nextNumber : lastId + "_1";
    }

    private String getNextNumber(String lastId) {
        if (GenericTools.isEmpty(lastId)) {
            return getNumber(1, 6);
        }
        try {
            Long.parseLong(lastId);
        } catch (NumberFormatException e) {
            //Then lastId is alphanumeric
            String alphaPrefix = getAlphaPrefix(lastId);
            String nextNumberSuffix = getNextNumberSuffix(lastId, lastId.length() - alphaPrefix.length());
            return alphaPrefix + nextNumberSuffix;
        }
        //Then lastId is numeric
        return getNextNumberSuffix(lastId, lastId.length());
    }

    private String getAlphaPrefix(String lastId) {
        String buffer = lastId;
        String numberAsString = "";
        while (buffer.length() > 0) {
            char character = buffer.charAt(buffer.length() - 1);
            try {
                Long.parseLong(character + "");
            } catch (NumberFormatException e) {
                return lastId.substring(0, lastId.lastIndexOf(numberAsString));
            }
            numberAsString = character + numberAsString;
            buffer = lastId.length() > 1 ? lastId.substring(0, buffer.length() - 1) : "";
        }
        return "";
    }

    private String getNextNumberSuffix(String lastId, int digits) {
        String numberAsString = lastId.substring(lastId.length() - digits, lastId.length());
        if (GenericTools.isEmpty(numberAsString)) {
            return "1";
        }
        long number = Long.parseLong(numberAsString);
        return getNumber(number + 1, digits);
    }

    protected List<Long> getEntityIdsByNumber(Enterprise enterprise, String number) {
        String queryString = getUniqueNumberVerificationQuery();

        Query query = entityManager.createQuery(queryString).
                setParameter("enterprises", enterprise.getAllRelatedEnterprises()).
                setParameter("number", number);

        if (queryString.contains(":numberPrefix")) {
            String prefix = number.contains("-") ? number.substring(0, number.lastIndexOf("-")) : number;
            query.setParameter("numberPrefix", prefix + "%");
        }

        return (List<Long>) query.getResultList();
    }

    private String getNumber(long number, int digits) {
        String numberAsString = number + "";
        while (numberAsString.length() < digits) {
            numberAsString = "0" + numberAsString;
        }
        return numberAsString;
    }

    protected abstract String getUniqueNumberVerificationQuery();

    protected abstract String getLastNumberQuery();

    public void persist(E entity) {
        log.info("PERSIST " + this.entityClass.getSimpleName());
        entityManager.persist(entity);
    }

    public void remove(E entity) {
        log.info("REMOVE " + this.entityClass.getSimpleName() + " #" + ((AbstractEntity) entity).getId());
        entityManager.remove(entity);
    }

    public E merge(E entity) {
        log.info("MERGE " + this.entityClass.getSimpleName() + " #" + ((AbstractEntity) entity).getId());
        return entityManager.merge(entity);
    }

    public void refresh(E entity) {
        log.info("REFRESH " + this.entityClass.getSimpleName() + " #" + ((AbstractEntity) entity).getId());
        entityManager.refresh(entity);
    }

    public E findById(K id) {
        log.info("FIND by id " + this.entityClass.getSimpleName() + " #" + id);
        return entityManager.find(entityClass, id);
    }

    public E flush(E entity) {
        log.info("FLUSH " + this.entityClass.getSimpleName() + " #" + ((AbstractEntity) entity).getId());
        entityManager.flush();
        return entity;
    }

    public void clearEntityManager() {
        entityManager.clear();
    }

    public void saveExternalLinkInfo(BusinessEntity entity, String linkName, String linkUrl) {
        String query = "UPDATE ";
        if (entity instanceof Client) {
            query += "CLIENT ";
        } else if (entity instanceof Product) {
            query += "PRODUCT ";
        } else if (entity instanceof Service) {
            query += "SERVICE ";
        } else if (entity instanceof Sale) {
            query += "SALE ";
        } else if (entity instanceof Expense) {
            query += "EXPENSE ";
        }

        query += "SET EXTERNAL_LINK_NAME =:linkName, EXTERNAL_LINK_URL =:linkUrl WHERE ID =:id";

        entityManager.createNativeQuery(query)
                .setParameter("linkName", linkName)
                .setParameter("linkUrl", linkUrl)
                .setParameter("id", entity.getId()).executeUpdate();
    }
}
